Исследование объявлений о продаже квартир¶

В вашем распоряжении данные сервиса Яндекс.Недвижимость — архив объявлений о продаже квартир в Санкт-Петербурге и соседних населённых пунктов за несколько лет. Нужно научиться определять рыночную стоимость объектов недвижимости. Ваша задача — установить параметры. Это позволит построить автоматизированную систему: она отследит аномалии и мошенническую деятельность.

По каждой квартире на продажу доступны два вида данных. Первые вписаны пользователем, вторые — получены автоматически на основе картографических данных. Например, расстояние до центра, аэропорта, ближайшего парка и водоёма.

Откройте файл с данными и изучите общую информацию.¶

In [1]:
import pandas as pd
import numpy as np
import math

import seaborn as sns
import matplotlib.pyplot as plt
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')
In [2]:
data = pd.read_csv('real_estate_data.csv', sep='\t')
In [3]:
display(data.head(), data.tail())
total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... kitchen_area balcony locality_name airports_nearest cityCenters_nearest parks_around3000 parks_nearest ponds_around3000 ponds_nearest days_exposition
0 20 13000000.0 108.0 2019-03-07T00:00:00 3 2.70 16.0 51.0 8 NaN ... 25.0 NaN Санкт-Петербург 18863.0 16028.0 1.0 482.0 2.0 755.0 NaN
1 7 3350000.0 40.4 2018-12-04T00:00:00 1 NaN 11.0 18.6 1 NaN ... 11.0 2.0 посёлок Шушары 12817.0 18603.0 0.0 NaN 0.0 NaN 81.0
2 10 5196000.0 56.0 2015-08-20T00:00:00 2 NaN 5.0 34.3 4 NaN ... 8.3 0.0 Санкт-Петербург 21741.0 13933.0 1.0 90.0 2.0 574.0 558.0
3 0 64900000.0 159.0 2015-07-24T00:00:00 3 NaN 14.0 NaN 9 NaN ... NaN 0.0 Санкт-Петербург 28098.0 6800.0 2.0 84.0 3.0 234.0 424.0
4 2 10000000.0 100.0 2018-06-19T00:00:00 2 3.03 14.0 32.0 13 NaN ... 41.0 NaN Санкт-Петербург 31856.0 8098.0 2.0 112.0 1.0 48.0 121.0

5 rows × 22 columns

total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... kitchen_area balcony locality_name airports_nearest cityCenters_nearest parks_around3000 parks_nearest ponds_around3000 ponds_nearest days_exposition
23694 9 9700000.0 133.81 2017-03-21T00:00:00 3 3.7 5.0 73.3 3 NaN ... 13.83 NaN Санкт-Петербург 24665.0 4232.0 1.0 796.0 3.0 381.0 NaN
23695 14 3100000.0 59.00 2018-01-15T00:00:00 3 NaN 5.0 38.0 4 NaN ... 8.50 NaN Тосно NaN NaN NaN NaN NaN NaN 45.0
23696 18 2500000.0 56.70 2018-02-11T00:00:00 2 NaN 3.0 29.7 1 NaN ... NaN NaN село Рождествено NaN NaN NaN NaN NaN NaN NaN
23697 13 11475000.0 76.75 2017-03-28T00:00:00 2 3.0 17.0 NaN 12 NaN ... 23.30 2.0 Санкт-Петербург 39140.0 10364.0 2.0 173.0 3.0 196.0 602.0
23698 4 1350000.0 32.30 2017-07-21T00:00:00 1 2.5 5.0 12.3 1 NaN ... 9.00 NaN поселок Новый Учхоз NaN NaN NaN NaN NaN NaN NaN

5 rows × 22 columns

In [4]:
# Проведем разведочный анализ данных
data.shape
Out[4]:
(23699, 22)
In [5]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 23699 entries, 0 to 23698
Data columns (total 22 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   total_images          23699 non-null  int64  
 1   last_price            23699 non-null  float64
 2   total_area            23699 non-null  float64
 3   first_day_exposition  23699 non-null  object 
 4   rooms                 23699 non-null  int64  
 5   ceiling_height        14504 non-null  float64
 6   floors_total          23613 non-null  float64
 7   living_area           21796 non-null  float64
 8   floor                 23699 non-null  int64  
 9   is_apartment          2775 non-null   object 
 10  studio                23699 non-null  bool   
 11  open_plan             23699 non-null  bool   
 12  kitchen_area          21421 non-null  float64
 13  balcony               12180 non-null  float64
 14  locality_name         23650 non-null  object 
 15  airports_nearest      18157 non-null  float64
 16  cityCenters_nearest   18180 non-null  float64
 17  parks_around3000      18181 non-null  float64
 18  parks_nearest         8079 non-null   float64
 19  ponds_around3000      18181 non-null  float64
 20  ponds_nearest         9110 non-null   float64
 21  days_exposition       20518 non-null  float64
dtypes: bool(2), float64(14), int64(3), object(3)
memory usage: 3.7+ MB
In [6]:
# Количество пропусков в датасете
data.isna().sum()
Out[6]:
total_images                0
last_price                  0
total_area                  0
first_day_exposition        0
rooms                       0
ceiling_height           9195
floors_total               86
living_area              1903
floor                       0
is_apartment            20924
studio                      0
open_plan                   0
kitchen_area             2278
balcony                 11519
locality_name              49
airports_nearest         5542
cityCenters_nearest      5519
parks_around3000         5518
parks_nearest           15620
ponds_around3000         5518
ponds_nearest           14589
days_exposition          3181
dtype: int64
In [7]:
# Количество явных дубликатов
data.duplicated().sum()
Out[7]:
0
In [8]:
data.hist(figsize=(15, 20));

Подведем итоги разведовочного анализа

  1. В столбцах floors_total, balcony, parks_around3000, ponds_around3000 и days_exposition тип данных должны быть целочисленными, is_apartment - булевым, а first_day_exposition - тип даты и времени
  2. Переименуем столбец cityCenters_nearest в соответствии со стилем snake_case
  3. Явных дубликатов не наблюдается
  4. Также, различное количество значений в столбцах указывает на возможное наличие в таблице пропусков. Пропуски могут быть связаны с "человеческим" фактором, то есть пользователь мог не указать конкретный параметр, предполагая его отсутствие (например, количество балконов или принадлежность к апартаментам); а могут быть связаны с техническими ошибками.
  5. Из полученных гистрограм мы видим, что некоторые колонки заставляют усомниться в качестве данных, например колонка ceiling_height с максимальной высотой потолка в 100 метров и колонка room с количеством ноль комнат

Данные представлены в виде таблицы, включающей в себя 23 698 строк и 22 столбца. При этом 3 столбца - с целочисленными данными (тип < int >), 14 столбцов - с числовыми данными типа < float >, 3 столбца типа < object > и 2 столбца с булевым типом (тип < bool >). Разберём, какие в таблице столбцы, и какую информацию они содержат:

total_images — число фотографий квартиры в объявлении;
last_price — цена на момент снятия с публикации;
total_area — площадь квартиры в квадратных метрах (м²);
first_day_exposition — дата публикации;
rooms — число комнат;
ceiling_height — высота потолков (м);
floors_total — всего этажей в доме;
living_area — жилая площадь в квадратных метрах(м²);
floor — этаж;
is_apartment — апартаменты;
studio — квартира-студия;
open_plan — свободная планировка;
kitchen_area — площадь кухни в квадратных метрах (м²);
balcony — число балконов;
locality_name — название населённого пункта;
airports_nearest — расстояние до ближайшего аэропорта в метрах (м);
cityCenters_nearest — расстояние до центра города (м);
parks_around3000 — число парков в радиусе 3 км;
parks_nearest — расстояние до ближайшего парка (м);
ponds_around3000 — число водоёмов в радиусе 3 км;
ponds_nearest — расстояние до ближайшего водоёма (м);
days_exposition — сколько дней было размещено объявление (от публикации до снятия).

Предобработка данных¶

Настало время заполнить пропущенные пропуски

**ceiling_height** - высота потолков

In [9]:
# Начнем по-порядку
# Колонку ceiling_height предлагаю запонить медианным значением
print('Медианная высота потолков:',data.ceiling_height.median())
data.ceiling_height = data.ceiling_height.fillna(2.65)
Медианная высота потолков: 2.65
In [10]:
data.ceiling_height.sort_values(ascending=False).head(60)
Out[10]:
22869    100.00
3148      32.00
22336     32.00
21377     27.50
4876      27.00
17857     27.00
5246      27.00
20478     27.00
22938     27.00
5807      27.00
21824     27.00
10773     27.00
5669      26.00
18545     25.00
4643      25.00
9379      25.00
11285     25.00
14382     25.00
355       25.00
6246      25.00
5076      24.00
20507     22.60
17496     20.00
15061     14.00
22309     10.30
5863       8.30
3474       8.00
15743      8.00
17442      8.00
20264      6.00
21227      5.80
1388       5.60
7578       5.50
12628      5.30
1026       5.30
464        5.20
1053       5.00
21923      4.90
2802       4.80
1300       4.70
19142      4.70
12401      4.65
2823       4.50
9783       4.50
10159      4.50
6802       4.50
13224      4.50
14519      4.50
3067       4.50
7521       4.45
8018       4.45
6728       4.40
11651      4.40
7274       4.40
4201       4.37
10754      4.30
17078      4.25
6901       4.20
11128      4.20
2843       4.20
Name: ceiling_height, dtype: float64
In [11]:
data.loc[(data.ceiling_height == 20), 'ceiling_height'] = 2.0
data.loc[(data.ceiling_height == 22.6), 'ceiling_height'] = 2.26
data.loc[(data.ceiling_height == 24), 'ceiling_height'] = 2.4
data.loc[(data.ceiling_height == 25), 'ceiling_height'] = 2.5
data.loc[(data.ceiling_height == 26), 'ceiling_height'] = 2.6
data.loc[(data.ceiling_height == 27), 'ceiling_height'] = 2.7
data.loc[(data.ceiling_height == 27.5), 'ceiling_height'] = 2.75
data.loc[(data.ceiling_height == 32), 'ceiling_height'] = 3.2

# Автомотизировать данный процесс и удалить выбивающиеся значения к сожалению у меня не получилось, 
# рассчитываю на вашу подсказку 

**floors_total** - всего этажей в доме

In [12]:
# Строим сводную таблицу с медианным значением
floor_median = data.pivot_table(index='floor', values='floors_total', aggfunc='median').reset_index()

# Строим цикл
for index in range(data.floor.min(), (data.floor.max()+1)):
    data.loc[data['floor'] == index, 'floors_total'] = \
    (data.loc[data['floor'] == index, 'floors_total'] \
    .fillna(floor_median[floor_median['floor'] == index]['floors_total'].sum()))
    
print('Пропущенных значений:', data.floors_total.isna().sum())

# Меняем тип на int
data.floors_total = data.floors_total.astype('int')
Пропущенных значений: 0

**living_area** - жилая площадь

In [13]:
"""
При определении жилой площади в расчёт берутся размеры только тех комнат, которые пригодны для проживания 
(спальня, детская, гостиная). К жилой площади не относятся кухни, коридоры, санузлы, кладовые, встроенные шкафы, 
балконы и лоджии
"""
# Прокоррелируем)) Найдём через корреляцию те столбцы, при помощи которых мы можем произвести заполнение.
сorr = data.corr()
print(сorr.sort_values(by='living_area', ascending=False)['living_area'].reset_index().head())

# Посчитаем усредненное значение жилой площади одной комнаты
df = round(data.pivot_table(index='rooms', values='living_area', aggfunc='mean')).reset_index()
df
          index  living_area
0   living_area     1.000000
1    total_area     0.939537
2         rooms     0.845977
3    last_price     0.566492
4  kitchen_area     0.428674
Out[13]:
rooms living_area
0 0 19.0
1 1 18.0
2 2 32.0
3 3 47.0
4 4 67.0
5 5 100.0
6 6 131.0
7 7 164.0
8 8 169.0
9 9 190.0
10 10 166.0
11 11 134.0
12 12 410.0
13 14 195.0
14 15 409.0
15 16 180.0
16 19 264.0
In [14]:
round(df.living_area.sum() / df.rooms.sum())
Out[14]:
19
Начну с того, что в колонке rooms отсутствуют пропуски - и это нам на руку. Также мы видим, что коррелируют, как минимум три первые колонки, и посчитали среднее значение жилой площади одной комнаты: Что в итоге ? Я предлагаю взять за основу усредненную площадь одной комнаты и умножить данное значение на количество комнат и так мы посчитаем жилую площадь всей квартиры
In [15]:
# Построим функцию
mean = 19
def replace_living(row):
    count = row['rooms']
    return count * mean
data['living_area'] = data.apply(replace_living, axis=1)

**is_apartment** - аппартаменты

In [16]:
# Посмотрим соотношение аппартаментов и не аппартаментов
data.groupby('is_apartment', as_index=False).agg({'rooms':'count'})
Out[16]:
is_apartment rooms
0 False 2725
1 True 50
In [17]:
"""Замена осуществляется на False, ввиду того, что пропущенные значения скорей всего означают, 
что аппартаменты не входят в предложение по продаже кваритр т.к. при отстутсвии апартаментов человек
не заполнял этот столбец"""

data.is_apartment = data.is_apartment.fillna(False)
# Заменяем тип данных на "bool"
data.is_apartment = data.is_apartment.astype(bool)

**kitchen_area** - площадь кухни

In [18]:
# Согласно строительным нормам кухня не может быть меньше 5м², предлагаю удалить все варианты ниже данного значения
data.drop(index=data.query('kitchen_area < 5').index,inplace=True)
Определить и восполнить пропущенные значение в колонке kitchen_area будет определенно невозможно, по ряду причин:

1. Площадь кухни не была учтена если это кухня-гостинная, либо - это квартира типа студии
2. Также в принципе невозможно оперделить площадь кухни из имеющихся данных и решение типа: "отнять жилую площадь от общей" нам не поможет - т.к. в датасете нет информации о С/У, кладовоке, гардеробе и т.д.

В данном случае предлагаю все пропущенные значения "восполнить нулями" - т.к. есть вероятность, что собственник не указал площадь кухни из-за первой причины.
In [19]:
data.kitchen_area = data.kitchen_area.fillna(0)

**balcony** - балкон

In [20]:
# Заполним пропущенные данные в колонке наличия балконов, где 0 - нет балкона. Также изменим тип данных на int
data.balcony = data.balcony.fillna(0).astype('int')

**locality_name** - название населенного пункта

In [21]:
# Исследуем столбец locality_name на предмет уникальных значений
print('Количество уникальных значений:', data['locality_name'].nunique())
data['locality_name'].unique()
Количество уникальных значений: 363
Out[21]:
array(['Санкт-Петербург', 'посёлок Шушары', 'городской посёлок Янино-1',
       'посёлок Парголово', 'посёлок Мурино', 'Ломоносов', 'Сертолово',
       'Петергоф', 'Пушкин', 'деревня Кудрово', 'Коммунар', 'Колпино',
       'поселок городского типа Красный Бор', 'Гатчина', 'поселок Мурино',
       'деревня Фёдоровское', 'Выборг', 'Кронштадт', 'Кировск',
       'деревня Новое Девяткино', 'посёлок Металлострой',
       'посёлок городского типа Лебяжье',
       'посёлок городского типа Сиверский', 'поселок Молодцово',
       'поселок городского типа Кузьмоловский',
       'садовое товарищество Новая Ропша', 'Павловск',
       'деревня Пикколово', 'Всеволожск', 'Волхов', 'Кингисепп',
       'Приозерск', 'Сестрорецк', 'деревня Куттузи', 'посёлок Аннино',
       'поселок городского типа Ефимовский', 'посёлок Плодовое',
       'деревня Заклинье', 'поселок Торковичи', 'поселок Первомайское',
       'Красное Село', 'посёлок Понтонный', 'Сясьстрой', 'деревня Старая',
       'деревня Лесколово', 'посёлок Новый Свет', 'Сланцы',
       'село Путилово', 'Ивангород', 'Мурино', 'Шлиссельбург',
       'Никольское', 'Зеленогорск', 'Сосновый Бор', 'поселок Новый Свет',
       'деревня Оржицы', 'деревня Кальтино', 'Кудрово',
       'поселок Романовка', 'посёлок Бугры', 'поселок Бугры',
       'поселок городского типа Рощино', 'Луга', 'Волосово', 'Отрадное',
       'село Павлово', 'поселок Оредеж', 'село Копорье',
       'посёлок городского типа Красный Бор', 'посёлок Молодёжное',
       'Тихвин', 'посёлок Победа', 'деревня Нурма',
       'поселок городского типа Синявино', 'Тосно',
       'посёлок городского типа Кузьмоловский', 'посёлок Стрельна',
       'Бокситогорск', 'посёлок Александровская', 'деревня Лопухинка',
       'Пикалёво', 'поселок Терволово',
       'поселок городского типа Советский', 'Кириши', 'Подпорожье',
       'посёлок Петровское', 'посёлок городского типа Токсово',
       'поселок Сельцо', 'посёлок городского типа Вырица',
       'деревня Кипень', 'деревня Келози', 'деревня Вартемяги',
       'посёлок Тельмана', 'поселок Севастьяново',
       'городской поселок Большая Ижора', nan,
       'городской посёлок Павлово', 'деревня Агалатово',
       'посёлок Новогорелово', 'городской посёлок Лесогорский',
       'деревня Лаголово', 'поселок Цвелодубово',
       'поселок городского типа Рахья', 'поселок городского типа Вырица',
       'деревня Белогорка', 'поселок Заводской',
       'городской посёлок Новоселье', 'деревня Большие Колпаны',
       'деревня Горбунки', 'деревня Батово', 'деревня Заневка',
       'деревня Иссад', 'Приморск', 'городской посёлок Фёдоровское',
       'деревня Мистолово', 'Новая Ладога', 'поселок Зимитицы',
       'поселок Барышево', 'деревня Разметелево',
       'поселок городского типа имени Свердлова', 'деревня Пеники',
       'поселок Рябово', 'деревня Пудомяги', 'поселок станции Корнево',
       'деревня Низино', 'деревня Бегуницы', 'посёлок Поляны',
       'городской посёлок Мга', 'поселок Елизаветино',
       'посёлок городского типа Кузнечное', 'деревня Колтуши',
       'поселок Запорожское', 'посёлок городского типа Рощино',
       'деревня Гостилицы', 'деревня Малое Карлино',
       'посёлок Мичуринское', 'посёлок городского типа имени Морозова',
       'посёлок Сосново', 'деревня Аро', 'поселок Ильичёво',
       'посёлок городского типа Тайцы', 'деревня Малое Верево',
       'деревня Извара', 'поселок станции Вещево', 'село Паша',
       'деревня Калитино', 'посёлок городского типа Ульяновка',
       'деревня Чудской Бор', 'поселок городского типа Дубровка',
       'деревня Мины', 'поселок Войсковицы',
       'посёлок городского типа имени Свердлова', 'деревня Коркино',
       'посёлок Ропша', 'поселок городского типа Приладожский',
       'посёлок Щеглово', 'посёлок Гаврилово', 'Лодейное Поле',
       'деревня Рабитицы', 'поселок городского типа Никольский',
       'деревня Кузьмолово', 'деревня Малые Колпаны', 'поселок Тельмана',
       'посёлок Петро-Славянка', 'городской посёлок Назия',
       'посёлок Репино', 'посёлок Ильичёво', 'поселок Углово',
       'поселок Старая Малукса', 'садовое товарищество Рахья',
       'поселок Аннино', 'поселок Победа', 'деревня Меньково',
       'деревня Старые Бегуницы', 'посёлок Сапёрный', 'поселок Семрино',
       'поселок Гаврилово', 'поселок Глажево', 'поселок Кобринское',
       'деревня Гарболово', 'деревня Юкки',
       'поселок станции Приветнинское', 'деревня Мануйлово',
       'деревня Пчева', 'поселок Поляны', 'поселок Цвылёво',
       'поселок Мельниково', 'посёлок Пудость', 'посёлок Усть-Луга',
       'Светогорск', 'Любань', 'поселок Селезнёво',
       'поселок городского типа Рябово', 'Каменногорск', 'деревня Кривко',
       'поселок Глебычево', 'деревня Парицы', 'поселок Жилпосёлок',
       'посёлок городского типа Мга', 'городской поселок Янино-1',
       'посёлок Войскорово', 'село Никольское', 'посёлок Терволово',
       'поселок Стеклянный', 'посёлок городского типа Важины',
       'посёлок Мыза-Ивановка', 'село Русско-Высоцкое',
       'поселок городского типа Лебяжье',
       'поселок городского типа Форносово', 'село Старая Ладога',
       'поселок Житково', 'городской посёлок Виллози', 'деревня Лампово',
       'деревня Шпаньково', 'деревня Лаврики', 'посёлок Сумино',
       'посёлок Возрождение', 'деревня Старосиверская',
       'посёлок Кикерино', 'поселок Возрождение',
       'деревня Старое Хинколово', 'посёлок Пригородный',
       'посёлок Торфяное', 'городской посёлок Будогощь',
       'поселок Суходолье', 'поселок Красная Долина', 'деревня Хапо-Ое',
       'поселок городского типа Дружная Горка', 'поселок Лисий Нос',
       'деревня Яльгелево', 'посёлок Стеклянный', 'село Рождествено',
       'деревня Старополье', 'посёлок Левашово', 'деревня Сяськелево',
       'деревня Камышовка',
       'садоводческое некоммерческое товарищество Лесная Поляна',
       'деревня Хязельки', 'поселок Жилгородок', 'деревня Ялгино',
       'поселок Новый Учхоз', 'городской посёлок Рощино',
       'поселок Гончарово', 'поселок Почап', 'посёлок Сапёрное',
       'посёлок Платформа 69-й километр', 'поселок Каложицы',
       'деревня Фалилеево', 'деревня Пельгора',
       'поселок городского типа Лесогорский', 'деревня Торошковичи',
       'посёлок Белоостров', 'посёлок Алексеевка', 'поселок Серебрянский',
       'поселок Лукаши', 'посёлок Песочный', 'поселок Петровское',
       'деревня Щеглово', 'поселок Мичуринское', 'деревня Тарасово',
       'поселок Кингисеппский',
       'посёлок при железнодорожной станции Вещево', 'поселок Ушаки',
       'деревня Котлы', 'деревня Сижно', 'деревня Торосово',
       'поселок городского типа Токсово', 'деревня Новолисино',
       'посёлок станции Громово', 'деревня Глинка', 'посёлок Мельниково',
       'поселок городского типа Назия', 'деревня Старая Пустошь',
       'поселок Коммунары', 'поселок Починок',
       'посёлок городского типа Вознесенье', 'деревня Разбегаево',
       'посёлок городского типа Рябово', 'поселок Гладкое',
       'посёлок при железнодорожной станции Приветнинское',
       'поселок Тёсово-4', 'посёлок Жилгородок', 'деревня Бор',
       'посёлок Коробицыно', 'деревня Большая Вруда', 'деревня Курковицы',
       'посёлок Лисий Нос', 'городской посёлок Советский',
       'посёлок Кобралово', 'деревня Суоранда', 'поселок Кобралово',
       'поселок городского типа Кондратьево',
       'коттеджный поселок Счастье', 'поселок Любань', 'деревня Реброво',
       'деревня Зимитицы', 'деревня Тойворово', 'поселок Семиозерье',
       'поселок Лесное', 'поселок Совхозный', 'поселок Усть-Луга',
       'посёлок Ленинское', 'посёлок Суйда',
       'посёлок городского типа Форносово', 'деревня Нижние Осельки',
       'посёлок станции Свирь', 'поселок Перово', 'Высоцк',
       'поселок Гарболово', 'село Шум', 'поселок Котельский',
       'поселок станции Лужайка', 'деревня Большая Пустомержа',
       'поселок Красносельское', 'деревня Вахнова Кара', 'деревня Пижма',
       'коттеджный поселок Кивеннапа Север', 'поселок Коробицыно',
       'поселок Ромашки', 'посёлок Перово', 'деревня Каськово',
       'деревня Куровицы', 'посёлок Плоское', 'поселок Сумино',
       'поселок городского типа Большая Ижора', 'поселок Кирпичное',
       'посёлок городского типа Павлово', 'деревня Ям-Тесово',
       'деревня Раздолье', 'деревня Терпилицы', 'посёлок Шугозеро',
       'деревня Ваганово', 'поселок Пушное', 'садовое товарищество Садко',
       'посёлок Усть-Ижора', 'деревня Выскатка',
       'городской посёлок Свирьстрой', 'поселок Громово',
       'деревня Кисельня', 'посёлок Старая Малукса',
       'деревня Трубников Бор', 'поселок Калитино',
       'посёлок Высокоключевой', 'садовое товарищество Приладожский',
       'посёлок Пансионат Зелёный Бор', 'деревня Ненимяки',
       'поселок Пансионат Зелёный Бор', 'деревня Снегирёвка',
       'деревня Рапполово', 'деревня Пустынка', 'поселок Рабитицы',
       'деревня Большой Сабск', 'деревня Русско', 'деревня Лупполово',
       'деревня Большое Рейзино', 'деревня Малая Романовка',
       'поселок Дружноселье', 'поселок Пчевжа', 'поселок Володарское',
       'деревня Нижняя', 'коттеджный посёлок Лесное', 'деревня Тихковицы',
       'деревня Борисова Грива', 'посёлок Дзержинского'], dtype=object)
In [22]:
# Замечены неявные дубликаты, устраним их... приведём значения к нижнему регистру для избавления от дубликатов
data['locality_name'] = data['locality_name'].str.lower()

# Выполним замену ошибки в написании 'поселок' вместо 'посёлок':
data['locality_name'] = data['locality_name'].str.replace('поселок','посёлок')

# Замена однотипный значений
data['locality_name'] = data.locality_name.str.replace('поселок городского типа', 'поселок')
data['locality_name'] = data.locality_name.str.replace('городской поселок', 'поселок')

# Уникальные значения в алфавитном порядке
set(data['locality_name'].unique())
Out[22]:
{nan,
 'бокситогорск',
 'волосово',
 'волхов',
 'всеволожск',
 'выборг',
 'высоцк',
 'гатчина',
 'городской посёлок большая ижора',
 'городской посёлок будогощь',
 'городской посёлок виллози',
 'городской посёлок лесогорский',
 'городской посёлок мга',
 'городской посёлок назия',
 'городской посёлок новоселье',
 'городской посёлок павлово',
 'городской посёлок рощино',
 'городской посёлок свирьстрой',
 'городской посёлок советский',
 'городской посёлок фёдоровское',
 'городской посёлок янино-1',
 'деревня агалатово',
 'деревня аро',
 'деревня батово',
 'деревня бегуницы',
 'деревня белогорка',
 'деревня большая вруда',
 'деревня большая пустомержа',
 'деревня большие колпаны',
 'деревня большое рейзино',
 'деревня большой сабск',
 'деревня бор',
 'деревня борисова грива',
 'деревня ваганово',
 'деревня вартемяги',
 'деревня вахнова кара',
 'деревня выскатка',
 'деревня гарболово',
 'деревня глинка',
 'деревня горбунки',
 'деревня гостилицы',
 'деревня заклинье',
 'деревня заневка',
 'деревня зимитицы',
 'деревня извара',
 'деревня иссад',
 'деревня калитино',
 'деревня кальтино',
 'деревня камышовка',
 'деревня каськово',
 'деревня келози',
 'деревня кипень',
 'деревня кисельня',
 'деревня колтуши',
 'деревня коркино',
 'деревня котлы',
 'деревня кривко',
 'деревня кудрово',
 'деревня кузьмолово',
 'деревня курковицы',
 'деревня куровицы',
 'деревня куттузи',
 'деревня лаврики',
 'деревня лаголово',
 'деревня лампово',
 'деревня лесколово',
 'деревня лопухинка',
 'деревня лупполово',
 'деревня малая романовка',
 'деревня малое верево',
 'деревня малое карлино',
 'деревня малые колпаны',
 'деревня мануйлово',
 'деревня меньково',
 'деревня мины',
 'деревня мистолово',
 'деревня ненимяки',
 'деревня нижние осельки',
 'деревня нижняя',
 'деревня низино',
 'деревня новое девяткино',
 'деревня новолисино',
 'деревня нурма',
 'деревня оржицы',
 'деревня парицы',
 'деревня пельгора',
 'деревня пеники',
 'деревня пижма',
 'деревня пикколово',
 'деревня пудомяги',
 'деревня пустынка',
 'деревня пчева',
 'деревня рабитицы',
 'деревня разбегаево',
 'деревня раздолье',
 'деревня разметелево',
 'деревня рапполово',
 'деревня реброво',
 'деревня русско',
 'деревня сижно',
 'деревня снегирёвка',
 'деревня старая',
 'деревня старая пустошь',
 'деревня старое хинколово',
 'деревня старополье',
 'деревня старосиверская',
 'деревня старые бегуницы',
 'деревня суоранда',
 'деревня сяськелево',
 'деревня тарасово',
 'деревня терпилицы',
 'деревня тихковицы',
 'деревня тойворово',
 'деревня торосово',
 'деревня торошковичи',
 'деревня трубников бор',
 'деревня фалилеево',
 'деревня фёдоровское',
 'деревня хапо-ое',
 'деревня хязельки',
 'деревня чудской бор',
 'деревня шпаньково',
 'деревня щеглово',
 'деревня юкки',
 'деревня ялгино',
 'деревня яльгелево',
 'деревня ям-тесово',
 'зеленогорск',
 'ивангород',
 'каменногорск',
 'кингисепп',
 'кириши',
 'кировск',
 'колпино',
 'коммунар',
 'коттеджный посёлок кивеннапа север',
 'коттеджный посёлок лесное',
 'коттеджный посёлок счастье',
 'красное село',
 'кронштадт',
 'кудрово',
 'лодейное поле',
 'ломоносов',
 'луга',
 'любань',
 'мурино',
 'никольское',
 'новая ладога',
 'отрадное',
 'павловск',
 'петергоф',
 'пикалёво',
 'подпорожье',
 'посёлок александровская',
 'посёлок алексеевка',
 'посёлок аннино',
 'посёлок барышево',
 'посёлок белоостров',
 'посёлок бугры',
 'посёлок возрождение',
 'посёлок войсковицы',
 'посёлок войскорово',
 'посёлок володарское',
 'посёлок высокоключевой',
 'посёлок гаврилово',
 'посёлок гарболово',
 'посёлок гладкое',
 'посёлок глажево',
 'посёлок глебычево',
 'посёлок гончарово',
 'посёлок городского типа большая ижора',
 'посёлок городского типа важины',
 'посёлок городского типа вознесенье',
 'посёлок городского типа вырица',
 'посёлок городского типа дружная горка',
 'посёлок городского типа дубровка',
 'посёлок городского типа ефимовский',
 'посёлок городского типа имени морозова',
 'посёлок городского типа имени свердлова',
 'посёлок городского типа кондратьево',
 'посёлок городского типа красный бор',
 'посёлок городского типа кузнечное',
 'посёлок городского типа кузьмоловский',
 'посёлок городского типа лебяжье',
 'посёлок городского типа лесогорский',
 'посёлок городского типа мга',
 'посёлок городского типа назия',
 'посёлок городского типа никольский',
 'посёлок городского типа павлово',
 'посёлок городского типа приладожский',
 'посёлок городского типа рахья',
 'посёлок городского типа рощино',
 'посёлок городского типа рябово',
 'посёлок городского типа сиверский',
 'посёлок городского типа синявино',
 'посёлок городского типа советский',
 'посёлок городского типа тайцы',
 'посёлок городского типа токсово',
 'посёлок городского типа ульяновка',
 'посёлок городского типа форносово',
 'посёлок громово',
 'посёлок дзержинского',
 'посёлок дружноселье',
 'посёлок елизаветино',
 'посёлок жилгородок',
 'посёлок жилпосёлок',
 'посёлок житково',
 'посёлок заводской',
 'посёлок запорожское',
 'посёлок зимитицы',
 'посёлок ильичёво',
 'посёлок калитино',
 'посёлок каложицы',
 'посёлок кикерино',
 'посёлок кингисеппский',
 'посёлок кирпичное',
 'посёлок кобралово',
 'посёлок кобринское',
 'посёлок коммунары',
 'посёлок коробицыно',
 'посёлок котельский',
 'посёлок красная долина',
 'посёлок красносельское',
 'посёлок левашово',
 'посёлок ленинское',
 'посёлок лесное',
 'посёлок лисий нос',
 'посёлок лукаши',
 'посёлок любань',
 'посёлок мельниково',
 'посёлок металлострой',
 'посёлок мичуринское',
 'посёлок молодцово',
 'посёлок молодёжное',
 'посёлок мурино',
 'посёлок мыза-ивановка',
 'посёлок новогорелово',
 'посёлок новый свет',
 'посёлок новый учхоз',
 'посёлок оредеж',
 'посёлок пансионат зелёный бор',
 'посёлок парголово',
 'посёлок первомайское',
 'посёлок перово',
 'посёлок песочный',
 'посёлок петро-славянка',
 'посёлок петровское',
 'посёлок платформа 69-й километр',
 'посёлок плодовое',
 'посёлок плоское',
 'посёлок победа',
 'посёлок поляны',
 'посёлок понтонный',
 'посёлок почап',
 'посёлок починок',
 'посёлок при железнодорожной станции вещево',
 'посёлок при железнодорожной станции приветнинское',
 'посёлок пригородный',
 'посёлок пудость',
 'посёлок пушное',
 'посёлок пчевжа',
 'посёлок рабитицы',
 'посёлок репино',
 'посёлок романовка',
 'посёлок ромашки',
 'посёлок ропша',
 'посёлок рябово',
 'посёлок сапёрное',
 'посёлок сапёрный',
 'посёлок севастьяново',
 'посёлок селезнёво',
 'посёлок сельцо',
 'посёлок семиозерье',
 'посёлок семрино',
 'посёлок серебрянский',
 'посёлок совхозный',
 'посёлок сосново',
 'посёлок станции вещево',
 'посёлок станции громово',
 'посёлок станции корнево',
 'посёлок станции лужайка',
 'посёлок станции приветнинское',
 'посёлок станции свирь',
 'посёлок старая малукса',
 'посёлок стеклянный',
 'посёлок стрельна',
 'посёлок суйда',
 'посёлок сумино',
 'посёлок суходолье',
 'посёлок тельмана',
 'посёлок терволово',
 'посёлок торковичи',
 'посёлок торфяное',
 'посёлок тёсово-4',
 'посёлок углово',
 'посёлок усть-ижора',
 'посёлок усть-луга',
 'посёлок ушаки',
 'посёлок цвелодубово',
 'посёлок цвылёво',
 'посёлок шугозеро',
 'посёлок шушары',
 'посёлок щеглово',
 'приморск',
 'приозерск',
 'пушкин',
 'садоводческое некоммерческое товарищество лесная поляна',
 'садовое товарищество новая ропша',
 'садовое товарищество приладожский',
 'садовое товарищество рахья',
 'садовое товарищество садко',
 'санкт-петербург',
 'светогорск',
 'село копорье',
 'село никольское',
 'село павлово',
 'село паша',
 'село путилово',
 'село рождествено',
 'село русско-высоцкое',
 'село старая ладога',
 'село шум',
 'сертолово',
 'сестрорецк',
 'сланцы',
 'сосновый бор',
 'сясьстрой',
 'тихвин',
 'тосно',
 'шлиссельбург'}
In [23]:
# В данной колонке 49 пропущенных значений, считаю верным решением их удалить т.к. у нас нет необходимой
# информации для его заполнения, такой как координаты или подобной информации...

data = data.dropna(subset=['locality_name']) # удаление пропусков
print("Пропущенных значений:", data.locality_name.isna().sum())

# Kол-во уникальных значений после удаления дубликатов
print('Количество населенных пунктов:', data.locality_name.nunique())
Пропущенных значений: 0
Количество населенных пунктов: 329

**airports_nearest** - расстояние до ближайшего аэропорта в метрах (м)

In [24]:
print(data.airports_nearest.describe())
count    18047.00000
mean     28807.62775
std      12617.14657
min          0.00000
25%      18590.00000
50%      26764.00000
75%      37294.00000
max      84869.00000
Name: airports_nearest, dtype: float64
Пропуски по столбцу, характеризующему расстояние до ближайшего аэропорта, составляет 5542. Это немаленькая величина, но медианное и среднее значение не подойдут для заполнения пропусков.


Эти данные получены автоматически на основе картографических данных. Это, скорее всего, говорит о том, что аэропорта либо нет в данном городе, либо он находится очень далеко (свыше максимального значения для данного столбца). Таким образом, эти значения не будут существенными при определении цен на недвижимость.

Считаю верным решением, оставить все пропущенные значения - без изменений!

И предварительно просмотрев оставшиеся 6 колонок, поняла - что с ними такая же история и также предлагаю оставить все пропущенные значения - без изменений! 💣

**city_centers_nearest** - расстояние до центра города (м);

In [25]:
# Приведем имя колонки cityCenters_nearest к единому стилю
data = data.rename(columns={'cityCenters_nearest':'city_centers_nearest'})
In [26]:
# Меняем тип данных в колонке first_day_exposition
data.first_day_exposition = pd.to_datetime(data.first_day_exposition)
In [27]:
# Пробелы восполнены, типы данных приведенны в порядок
print(data.isna().sum())
data.info()
total_images                0
last_price                  0
total_area                  0
first_day_exposition        0
rooms                       0
ceiling_height              0
floors_total                0
living_area                 0
floor                       0
is_apartment                0
studio                      0
open_plan                   0
kitchen_area                0
balcony                     0
locality_name               0
airports_nearest         5498
city_centers_nearest     5475
parks_around3000         5474
parks_nearest           15501
ponds_around3000         5474
ponds_nearest           14498
days_exposition          3166
dtype: int64
<class 'pandas.core.frame.DataFrame'>
Int64Index: 23545 entries, 0 to 23698
Data columns (total 22 columns):
 #   Column                Non-Null Count  Dtype         
---  ------                --------------  -----         
 0   total_images          23545 non-null  int64         
 1   last_price            23545 non-null  float64       
 2   total_area            23545 non-null  float64       
 3   first_day_exposition  23545 non-null  datetime64[ns]
 4   rooms                 23545 non-null  int64         
 5   ceiling_height        23545 non-null  float64       
 6   floors_total          23545 non-null  int64         
 7   living_area           23545 non-null  int64         
 8   floor                 23545 non-null  int64         
 9   is_apartment          23545 non-null  bool          
 10  studio                23545 non-null  bool          
 11  open_plan             23545 non-null  bool          
 12  kitchen_area          23545 non-null  float64       
 13  balcony               23545 non-null  int64         
 14  locality_name         23545 non-null  object        
 15  airports_nearest      18047 non-null  float64       
 16  city_centers_nearest  18070 non-null  float64       
 17  parks_around3000      18071 non-null  float64       
 18  parks_nearest         8044 non-null   float64       
 19  ponds_around3000      18071 non-null  float64       
 20  ponds_nearest         9047 non-null   float64       
 21  days_exposition       20379 non-null  float64       
dtypes: bool(3), datetime64[ns](1), float64(11), int64(6), object(1)
memory usage: 3.7+ MB
По результату проведённой обработки данных, были достигнуты вышеизложенные цели: заполненны пропуски где это было возможно, и измененны типы данных. Также можно сказать что довольно большая часть данных, которая может влиять на итоговую статистику, а так же на решение о прибретении квартир, потеряна при выгрузке таблицы либо отсутствует. Об этом обязательно необходимо сообщить ответственным сотрудникам по выгрузке и сбору данных.

Посчитайте и добавьте в таблицу новые столбцы¶

Цена одного квадратного метра

In [28]:
data['price_per_square_meter'] = data['last_price']/data['total_area']
In [29]:
data.head()
Out[29]:
total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... balcony locality_name airports_nearest city_centers_nearest parks_around3000 parks_nearest ponds_around3000 ponds_nearest days_exposition price_per_square_meter
0 20 13000000.0 108.0 2019-03-07 3 2.70 16 57 8 False ... 0 санкт-петербург 18863.0 16028.0 1.0 482.0 2.0 755.0 NaN 120370.370370
1 7 3350000.0 40.4 2018-12-04 1 2.65 11 19 1 False ... 2 посёлок шушары 12817.0 18603.0 0.0 NaN 0.0 NaN 81.0 82920.792079
2 10 5196000.0 56.0 2015-08-20 2 2.65 5 38 4 False ... 0 санкт-петербург 21741.0 13933.0 1.0 90.0 2.0 574.0 558.0 92785.714286
3 0 64900000.0 159.0 2015-07-24 3 2.65 14 57 9 False ... 0 санкт-петербург 28098.0 6800.0 2.0 84.0 3.0 234.0 424.0 408176.100629
4 2 10000000.0 100.0 2018-06-19 2 3.03 14 38 13 False ... 0 санкт-петербург 31856.0 8098.0 2.0 112.0 1.0 48.0 121.0 100000.000000

5 rows × 23 columns

In [30]:
# Максимальная цена за квадрат
print('Максимальная цена за квадрат:', data.price_per_square_meter.max())
# Средняя цена за квадрат
print('Средняя цена за квадрат:', data.price_per_square_meter.mean())
# Медианная цена за квадрат
print('Медианная цена за квадрат:', data.price_per_square_meter.median())
# Минимальная цена за квадрат
print('Минимальная цена за квадрат:', data.price_per_square_meter.min())
Максимальная цена за квадрат: 1907500.0
Средняя цена за квадрат: 99467.63451601329
Медианная цена за квадрат: 95000.0
Минимальная цена за квадрат: 111.8348623853211

День недели, месяц, год публикации объявления

In [31]:
# День недели
data['day_publication'] = data['first_day_exposition'].dt.weekday
# Месяц
data['month_publication'] = data['first_day_exposition'].dt.month
# Год
data['years_publication'] = data['first_day_exposition'].dt.year
data.head()
Out[31]:
total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... city_centers_nearest parks_around3000 parks_nearest ponds_around3000 ponds_nearest days_exposition price_per_square_meter day_publication month_publication years_publication
0 20 13000000.0 108.0 2019-03-07 3 2.70 16 57 8 False ... 16028.0 1.0 482.0 2.0 755.0 NaN 120370.370370 3 3 2019
1 7 3350000.0 40.4 2018-12-04 1 2.65 11 19 1 False ... 18603.0 0.0 NaN 0.0 NaN 81.0 82920.792079 1 12 2018
2 10 5196000.0 56.0 2015-08-20 2 2.65 5 38 4 False ... 13933.0 1.0 90.0 2.0 574.0 558.0 92785.714286 3 8 2015
3 0 64900000.0 159.0 2015-07-24 3 2.65 14 57 9 False ... 6800.0 2.0 84.0 3.0 234.0 424.0 408176.100629 4 7 2015
4 2 10000000.0 100.0 2018-06-19 2 3.03 14 38 13 False ... 8098.0 2.0 112.0 1.0 48.0 121.0 100000.000000 1 6 2018

5 rows × 26 columns

In [32]:
# Посмотрим в какой год было построенно больше всего домов
data['years_publication'].value_counts()
Out[32]:
2018    8473
2017    8153
2019    2867
2016    2746
2015    1170
2014     136
Name: years_publication, dtype: int64
Очевидно, что количество предложений на рынке недвижимости было пиковым значением в 2017 и 2018 годах, а вот в 2019 количество предложений сократилось в 4 раза и вернулось на уровень 2016 года - на мой взгляд это связанно с переходом строительства на систему эскроу-счетов и в следсивие этого число новых проектов существенно снизилось

Разделим на группы этажи квартир и добавим их в таблицу; варианты — первый, последний, другой

In [33]:
# Посторим функцию
def floor(row):
    if row['floor'] == 1:
        return 'первый'
    if row['floor'] == row['floors_total']:
        return 'последний'
    else:
        return 'другой'
data['floor_group'] = data.apply(floor, axis=1)
In [34]:
data.head()
Out[34]:
total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... parks_around3000 parks_nearest ponds_around3000 ponds_nearest days_exposition price_per_square_meter day_publication month_publication years_publication floor_group
0 20 13000000.0 108.0 2019-03-07 3 2.70 16 57 8 False ... 1.0 482.0 2.0 755.0 NaN 120370.370370 3 3 2019 другой
1 7 3350000.0 40.4 2018-12-04 1 2.65 11 19 1 False ... 0.0 NaN 0.0 NaN 81.0 82920.792079 1 12 2018 первый
2 10 5196000.0 56.0 2015-08-20 2 2.65 5 38 4 False ... 1.0 90.0 2.0 574.0 558.0 92785.714286 3 8 2015 другой
3 0 64900000.0 159.0 2015-07-24 3 2.65 14 57 9 False ... 2.0 84.0 3.0 234.0 424.0 408176.100629 4 7 2015 другой
4 2 10000000.0 100.0 2018-06-19 2 3.03 14 38 13 False ... 2.0 112.0 1.0 48.0 121.0 100000.000000 1 6 2018 другой

5 rows × 27 columns

In [35]:
data['floor_group'].value_counts()
Out[35]:
другой       17352
последний     3304
первый        2889
Name: floor_group, dtype: int64
Cудя по количеству предложений, более актуальны квартиры на любом этаже кроме первого и последнего - классика жанра

Расстояние до центра города в километрах

In [36]:
# Округляем 
data['distance_city_center_km'] = round(data['city_centers_nearest'] / 1000)
try:
    data['distance_city_center_km'] =  round(data['distance_city_center_km'], 2).astype('Int32')
    print('Все ОК!')
except:
    print('Необходимо изменить номер бита переменной типа int')
    
data.head()
Все ОК!
Out[36]:
total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... parks_nearest ponds_around3000 ponds_nearest days_exposition price_per_square_meter day_publication month_publication years_publication floor_group distance_city_center_km
0 20 13000000.0 108.0 2019-03-07 3 2.70 16 57 8 False ... 482.0 2.0 755.0 NaN 120370.370370 3 3 2019 другой 16
1 7 3350000.0 40.4 2018-12-04 1 2.65 11 19 1 False ... NaN 0.0 NaN 81.0 82920.792079 1 12 2018 первый 19
2 10 5196000.0 56.0 2015-08-20 2 2.65 5 38 4 False ... 90.0 2.0 574.0 558.0 92785.714286 3 8 2015 другой 14
3 0 64900000.0 159.0 2015-07-24 3 2.65 14 57 9 False ... 84.0 3.0 234.0 424.0 408176.100629 4 7 2015 другой 7
4 2 10000000.0 100.0 2018-06-19 2 3.03 14 38 13 False ... 112.0 1.0 48.0 121.0 100000.000000 1 6 2018 другой 8

5 rows × 28 columns

Проведите исследовательский анализ данных¶

Изучим следующие параметры: площадь, цена, число комнат, высота потолков. Построим гистограммы для каждого параметра

Площадь

In [37]:
print('Анализ общей площади')
print(data.total_area.describe())
print('-' * 35)
print('Анализ жилой площади')
print(data.living_area.describe())
Анализ общей площади
count    23545.000000
mean        60.434825
std         35.696508
min         12.000000
25%         40.000000
50%         52.000000
75%         70.000000
max        900.000000
Name: total_area, dtype: float64
-----------------------------------
Анализ жилой площади
count    23545.000000
mean        39.366192
std         20.512356
min          0.000000
25%         19.000000
50%         38.000000
75%         57.000000
max        361.000000
Name: living_area, dtype: float64
In [38]:
print('Диаграмма размаха')
data.boxplot(column=['total_area'], figsize=(10,7), grid=True)
plt.ylabel('Площадь в квадратных метрах')
plt.show()
Диаграмма размаха
In [39]:
data.hist('total_area', bins=100, figsize=(10,7));
Предлагаю отчесь все значения от 20м² до 100м², для проведения качественного анализа
In [40]:
df = data.query('total_area >= 20 & total_area <= 100')
In [41]:
print('Диаграмма размаха площади после отсечения выбивающихся значений')
df.boxplot(column=['total_area'], figsize=(10,7), grid=True)
plt.ylabel('Площадь в квадратных метрах')
plt.show()
Диаграмма размаха площади после отсечения выбивающихся значений
In [42]:
df.hist('total_area', bins=100, figsize=(10,7));

Площадь кухни

In [43]:
data.kitchen_area.describe()
Out[43]:
count    23545.000000
mean         9.577433
std          6.419919
min          0.000000
25%          6.400000
50%          9.000000
75%         11.500000
max        112.000000
Name: kitchen_area, dtype: float64
In [44]:
print('Диаграмма размаха площади кухни')
df.boxplot('kitchen_area', figsize=(10,5))
plt.ylabel('площадь кухни')
plt.show()
Диаграмма размаха площади кухни
In [45]:
# Предлагаю отсесь все значения выходящие от 5 до 17 м²
df = df.query('kitchen_area > 5 and kitchen_area <= 17')
In [46]:
print('Диаграмма размаха площади кухни')
df.boxplot('kitchen_area', figsize=(10,5))
plt.ylabel('площадь кухни')
plt.show()
df.hist('kitchen_area', bins= 100, figsize=(10,7));
Диаграмма размаха площади кухни

Цена

In [47]:
print('Диаграмма размаха цены предложений')
df.boxplot('last_price', figsize=(10,7))
plt.ylabel('Цена на момент снятия с публикации')
plt.show()
df.hist('last_price', bins=100, range=(0,2e+07), figsize=(10,7));
Диаграмма размаха цены предложений
In [48]:
df = df.query('last_price <= 9000000')
In [49]:
df.plot(kind='hist', y='last_price', title='Распределение стоимости квартир',grid=True, bins=50, figsize=(15,7))
plt.ylabel('количество предложений')
plt.xlabel('цена на момент снятия с публикации')
plt.show()
Из графика видно, что наиболее часто встречаемая цена за квартиру 3.9 - 4 млн.руб

Количество комнат

In [50]:
df.plot(kind='hist',y='rooms',title='Распределение квартир по количество комнат',grid=True, bins=15, figsize=(10,5))
plt.xlabel('Количество комнат')
plt.ylabel('Количество предложений')
plt.show()

Высота потолков

In [51]:
print('Диаграмма размаха высоты потолков')
df.boxplot(column=['ceiling_height'], figsize=(10,5), grid=True)
plt.ylabel('Высота потолков')
plt.show()
Диаграмма размаха высоты потолков
Опираясь на график boxplot, отсечем квартиры с потолками ниже 2.4 и выше 2.8 метров
In [52]:
df = df.query('ceiling_height > 2.4 and ceiling_height < 2.8')
In [53]:
print('Диаграмма размаха высоты потолков')
df.boxplot(column=['ceiling_height'], figsize=(10,5), grid=True)
plt.ylabel('Высота потолков')
plt.show()
Диаграмма размаха высоты потолков
In [54]:
df.plot(kind='hist',y='ceiling_height',grid=True, bins=50, figsize=(10,5), \
        title='Распределение предложений по высоте потолков')
plt.xlabel('Высота потолоков')
plt.ylabel('Количество предложений')
plt.show()
Опираясь на результаты проведенного анализа, определенно можно заявить: большинство предложение на рынке, это 1-2 комнатные квартиры в ценовом диапозоне 4 млн.руб. с общей площадью до 50м², где площадь кухни - 6м², а средняя высота потолка 2.65 метра - по всей видимости - это дома хрущёвской постройки

Этаж квартиры

In [55]:
df.floor.describe()
Out[55]:
count    15226.000000
mean         5.931433
std          4.811797
min          1.000000
25%          2.000000
50%          5.000000
75%          8.000000
max         27.000000
Name: floor, dtype: float64
In [56]:
df.plot(kind='hist',y='floor',grid=True, bins=100, figsize=(10,5), \
        title='Распределение предложений по этажности')
plt.xlabel('Этаж квартиры')
plt.ylabel('Количество предложений')
plt.show()

Тип этажа квартиры («первый», «последний», «другой»)

In [57]:
df.pivot_table(index='floor_group', values='last_price').sort_values(by='last_price', ascending=True) \
  .plot(grid=True, figsize=(10, 5),linewidth=3, title='Зависимость цены от этажа расположения квартиры')

plt.xlabel('Этаж расположения квартиры')
plt.ylabel('Цена')
plt.show()
In [58]:
df.pivot_table(index='floor_group',values='last_price',aggfunc='count') \
  .plot.pie(y='last_price', figsize=(10,7), label='', title='График количественного отношения распределения предложений в зависимости от этажа')

plt.show()

Общее количество этажей в доме

In [59]:
df.boxplot(column=['floors_total'], grid=True, figsize=(10,5))
df.plot.hist(y='floors_total',grid=True, bins=100, figsize=(10,5), title='Всего этажей в доме');
Глядя на графики, становится более очевидно, что основное количество предложений по продаже квартиры на рынке исходит от собственников пятиэтажных "хрущевок"

Расстояние до центра города в метрах

In [60]:
df = df.query('city_centers_nearest > 6000 and city_centers_nearest < 22000')
df.boxplot('city_centers_nearest', figsize=(10,5));
In [61]:
df.hist('city_centers_nearest', bins=100, figsize=(10,5));

Расстояние до ближайшего аэропорта

In [62]:
df.boxplot('airports_nearest', figsize=(10,5))
df.hist('airports_nearest', bins=100, figsize=(10,5));

Расстояние до ближайшего парка

In [63]:
df.boxplot('airports_nearest', figsize=(10,5))
df.hist('parks_nearest', bins=100, figsize=(10,5));

День и месяц публикации объявления

In [64]:
df.hist('day_publication', bins=50, figsize=(10,5));
df.hist('month_publication', bins=50, figsize=(10,5));
Определенно мы видим, что чаще все люди задумываются о продаже с февраля по апрель и с августа по ноябрь. И размещают объявление в будние рабочие дни. Также видим, что в большинстве из случаев парки располагаются в пешой доступности, а расстояние до аэропорта и до центра в пределах часовой езды на такси.

Изучим время продажи квартиры

In [65]:
data.days_exposition.describe()
Out[65]:
count    20379.000000
mean       180.839197
std        219.812620
min          1.000000
25%         45.000000
50%         95.000000
75%        231.000000
max       1580.000000
Name: days_exposition, dtype: float64
In [66]:
print('Время продаж')
df.boxplot('days_exposition', figsize=(10,5))
plt.ylabel('Время продажи квартиры')
plt.show()
Время продаж
In [67]:
df = df.query('days_exposition < 250')
In [68]:
print('Время продаж')
df.boxplot('days_exposition', figsize=(8,5))
plt.ylabel('Время продажи квартиры')
plt.show()
Время продаж
In [69]:
df.plot(kind='hist',y='days_exposition',title='Распределение количества продаж по времени', bins=100, figsize=(10,5))

plt.xlabel('Сколько дней было размещено объявление (от публикации до снятия)')
plt.ylabel('Количество объявлений')
plt.show()
В среднем продажа среднестатистичекой квартиры занимает 2-3 месяца в зависимотсти от сопутствующих факторов(площади, цены, удалённости от центра)Несмотря на то, что мы имеем огромное число квартир, которые так и не были проданы, мы имеем следующую тенденцию. Число квартир убывает с ростом срока продажи. Другими словами, если квартира "хорошая", она имеет высокий шанс быть проданой в течение полугода. Далее ее шанс на продажу падает. В связи с этим и количество продаж квартир за 1-2 года в несколько раз меньше, чем за год; а за 2-3 года их число насчитывает только десятки.

Какие факторы больше всего влияют на общую (полную) стоимость объекта?
Изучим, зависит ли цена от площади, числа комнат, удалённости от центра

In [70]:
df['city_centers_nearest'] = df['city_centers_nearest']/1000
In [71]:
# Сводную таблицу: проверим зависимость цены с площадью, с количеством комнат и с расстоянием до центра
df = df.pivot_table(index='last_price', values=['total_area','rooms','city_centers_nearest'])
df.reset_index().head()
Out[71]:
last_price city_centers_nearest rooms total_area
0 1200000.0 12.313 4.0 49.10
1 1780000.0 18.217 1.0 37.00
2 1800000.0 21.502 1.0 32.90
3 1938000.0 21.502 1.0 32.30
4 1961100.0 18.500 1.0 43.58
In [72]:
# Для наглядности построим cкаттерплот
df.plot(style='o',grid=True, figsize=(12, 5), title='График зависимости цены от площади, числа комнат, удалённости от центра')
plt.xlabel('Цена на момент снятия с публикации')
plt.ylabel('Удалённость от центра\n Число комнат\n Площадь')
plt.show()

Посчитайте среднюю цену одного квадратного метра в 10 населённых пунктах с наибольшим числом объявлений

In [73]:
top_10 = data.pivot_table(index='locality_name', values='first_day_exposition', aggfunc='count').sort_values(by='first_day_exposition', ascending=False).head(10)
top_10.columns=['number_of_ads']
top_10.reset_index()
Out[73]:
locality_name number_of_ads
0 санкт-петербург 15671
1 посёлок мурино 552
2 посёлок шушары 438
3 всеволожск 398
4 пушкин 366
5 колпино 338
6 посёлок парголово 327
7 гатчина 302
8 деревня кудрово 299
9 выборг 236
In [74]:
top_10['cost_per_meter_mean'] = data.pivot_table(index='locality_name', values='price_per_square_meter', aggfunc='mean')
top_10s = top_10.sort_values(by='cost_per_meter_mean', ascending=False).reset_index()

# Построим график
fig, ax = plt.subplots(figsize=(10,5))
ax.set(title = 'Стоимость квадратного метра по населённым пунктам с ТОП-10 числом объявлений',
       xlabel='населённый пункт',
       ylabel='цена м2, ден.ед.')
ax.plot(top_10s['locality_name'], top_10s['cost_per_meter_mean'], 'o-')
plt.xticks(rotation='vertical')
plt.show()
In [75]:
# Выделим самую высокую и низкую цену
top_10s.style.format({'cost_per_meter_mean':'{:.2f} руб.'}) \
       .highlight_max(color='yellowgreen', subset='cost_per_meter_mean') \
       .highlight_min(color='coral', subset='cost_per_meter_mean')
Out[75]:
  locality_name number_of_ads cost_per_meter_mean
0 санкт-петербург 15671 114836.47 руб.
1 пушкин 366 103188.21 руб.
2 деревня кудрово 299 92473.55 руб.
3 посёлок парголово 327 90175.91 руб.
4 посёлок мурино 552 85577.05 руб.
5 посёлок шушары 438 78513.33 руб.
6 колпино 338 75424.58 руб.
7 гатчина 302 68846.42 руб.
8 всеволожск 398 68654.47 руб.
9 выборг 236 58188.87 руб.
In [76]:
top_10.pivot_table(index='locality_name',values='cost_per_meter_mean') \
      .plot.pie(y='cost_per_meter_mean', figsize=(7,7), legend=False, label='', \
        title='Населённые пункты с самой высокой и низкой стоимостью жилья')
plt.show()

Опишите, как стоимость объектов зависит от расстояния до центра города

In [77]:
spb = data.loc[data['locality_name'] == 'санкт-петербург']
spb['city_centers_nearest'].isna().sum()
Out[77]:
61
In [78]:
spb = spb.dropna(subset=['city_centers_nearest'])
spb['city_centers_nearest_km'] = data.distance_city_center_km

spb['city_centers_nearest_km'].describe()
Out[78]:
count      15610.0
mean     11.596861
std       4.864728
min            0.0
25%            8.0
50%           12.0
75%           15.0
max           29.0
Name: city_centers_nearest_km, dtype: Float64
In [79]:
print('Диаграмма размаха удалённости от центра')
spb.boxplot('city_centers_nearest_km', figsize=(10,5))

plt.ylabel('Километры от центра города')
plt.show()
Диаграмма размаха удалённости от центра
In [80]:
spb = spb.loc[spb['city_centers_nearest_km'] < 24]
print('Диаграмма размаха удалённости от центра')
spb.boxplot('city_centers_nearest_km', figsize=(10,5))

plt.ylabel('Километры от центра города')
plt.show()
Диаграмма размаха удалённости от центра
In [81]:
mean_cost_per_km = spb['last_price'].sum()/spb['city_centers_nearest_km'].sum()
print(f'Средняя цена для каждого километра составляет {mean_cost_per_km:.0f} руб.')
Средняя цена для каждого километра составляет 702972 руб.

Построим график: он будет показывать, как цена зависит от удалённости от центра. А так же определим границу центральной зоны.

In [82]:
spb.pivot_table(index='city_centers_nearest_km', values='last_price', aggfunc='mean') \
   .plot(figsize=(10,5), grid=True, linewidth=3, title='График зависимости цены от удалённости от центра')
plt.xlabel('Километры от центра города')
plt.ylabel('Средняя стоимость предложения')
plt.show()

spb.pivot_table(index='city_centers_nearest_km', values='last_price', aggfunc='mean') \
   .plot(kind='bar', figsize=(10,5))
plt.xlabel('Километры от центра города')
plt.ylabel('Средняя стоимость предложения')
plt.show()
Очевидно, что падение цены на графике сильно изменилось при достижении значения в 6-7 км, это говорит нам, что центральным районом можно считать все квартиры в удалении от центра не более чем на 7 км.

Анализ квартир в центре
Выделим квартиры в центре Санкт-Петербурга в отдельный срез данных.

In [83]:
spb_centre = spb.loc[spb['city_centers_nearest_km'] < 7]
analysis_spb = spb_centre[['locality_name','total_area','living_area','kitchen_area','last_price']].reset_index(drop=True)
analysis_spb.corr()
Out[83]:
total_area living_area kitchen_area last_price
total_area 1.000000 0.734466 0.454940 0.604567
living_area 0.734466 1.000000 0.242428 0.303235
kitchen_area 0.454940 0.242428 1.000000 0.324052
last_price 0.604567 0.303235 0.324052 1.000000
In [84]:
analysis_spb.describe()
Out[84]:
total_area living_area kitchen_area last_price
count 3093.000000 3093.000000 3093.000000 3.093000e+03
mean 94.902632 54.978985 13.256735 1.494719e+07
std 60.312563 28.300627 9.630583 2.514233e+07
min 12.000000 0.000000 0.000000 1.600000e+06
25% 59.000000 38.000000 8.300000 6.800000e+06
50% 80.500000 57.000000 11.500000 9.400000e+06
75% 111.300000 76.000000 16.000000 1.449000e+07
max 631.200000 361.000000 107.000000 7.630000e+08
In [85]:
pd.plotting.scatter_matrix(analysis_spb, figsize=(10, 10)) 
plt.show()
In [86]:
analysis_spb_2 = spb_centre[['last_price','rooms','floor','city_centers_nearest_km','first_day_exposition']]
pd.plotting.scatter_matrix(analysis_spb_2, figsize=(10, 10))
plt.show()
In [87]:
spb_centre.pivot_table(index=['rooms'], values='last_price', aggfunc=['mean', 'median']) \
          .plot(kind='bar', grid=True, linewidth=3, alpha=0.6,figsize=(15,5), title='Зависимость цены от количества комнат')

plt.xlabel('Количество комнат')
plt.ylabel('Цена')
plt.show()
In [88]:
spb_centre.pivot_table(index=['floor_group'], values='last_price', aggfunc=['mean', 'median']) \
          .plot(kind='bar', grid=True,linewidth=3, alpha=0.7,figsize=(15,5), title='Зависимость цены от этажа')

plt.xlabel('Этаж')
plt.ylabel('Цена')
plt.show()
In [89]:
spb_centre.pivot_table(index=['day_publication'], values='last_price', aggfunc=['mean', 'median']) \
          .plot(grid=True,linewidth=3, alpha=0.7,figsize=(15,5), title='Зависимость цены от дня недели размещения объявления')

plt.xlabel('День размещения объявления')
plt.ylabel('Цена')
plt.show()
In [90]:
spb_centre.pivot_table(index=['month_publication'], values='last_price', aggfunc=['mean', 'median']) \
          .plot(grid=True,linewidth=3, alpha=0.7,figsize=(15,5), title='Зависимость цены от месяца размещения объявления')

plt.xlabel('Месяц размещения объявления')
plt.ylabel('Цена')
plt.show()
In [91]:
spb_centre.pivot_table(index=['years_publication'], values='last_price', aggfunc=['mean', 'median']) \
          .plot(grid=True,linewidth=3, alpha=0.7,figsize=(15,5), title='Зависимость цены от года размещения объявления')

plt.xlabel('Год размещения объявления')
plt.ylabel('Цена')
plt.show()
В целом из графиков можно сделать вывод о том, что цена объявления квартир в центре города не зависит от удалённости от центра, зависимость цены менятся только от площади и количесва комнат, так же ценятся квартиры в центре города не выше 10 этажа. Изучив зависимость этажности и удаления от центра можно заметить, что чем дальше от центра города, тем выше этаж квартиры в объявлении. А вот по дате размещения объявления заметен основной пик в стоимости жилья в 2014-2015 годах с плавным снижением к 2018 году, в то время как по остальным предложениям стоимость жилья за пределами центра активно растёт начиная с 2017 года.

Общий вывод¶

В данном исследовании были проанализированы объявления о продаже квартир в Санкт-Петербурге и в его пригородах. В ходе подготовки данных к анализу я столкнулась с большим количеством пропусков в колонках, которые могут влиять как на среднюю цену предложения квартир на рынке, так и на анализ выделенных групп в частности. Пропуски в некоторых колонках были восполнены средним либо медианным значением или же вовсе, восполнены нулями из-за отсутствия тех или иных параметров. Были также и такие колонки, где данные получены автоматически на основе картографических данных и пропуски в них были оставлены без изменения.


По результату проделанной работы по обработке и анализу недостающих данных, несомненно надо обратить внимание коллег по подготовке и сбору данных на заполняемость ячеек, либо на корректность выгрузки данных из программы. Ошибок связанных с человеческим фактором(опечаток и пр.) довольно мало, что сильно облегчило труд по предобработке материала для анализа.

После избавления пропусков и выбивающихся значений был произведен подсчёт средней стоимости квадратного метра жилья, стоимость каждого километра удаления от центра, выделен сегмент квартир расположенных в центре и проанализированы их параметры, а так же проанализирована скорость продажи объявления с даты его размещения, факторы влияющие на это и проанализирована статистика продаж в зависимости от периода размещения объявления.

Для большей наглядности при анализе использовались графики, из которых лего можно сделать вывод о том, что лидером по размещению объявлений о продаже недвижимости является "Санкт-Петербург", несмотря на это средняя цена квадратного метра приблизительно такая же как и по области. Тенденция рынка такова, что начиная с 2017 года активно растёт спрос на жильё за пределами центра Петербурга, при этом люди в основной своей массе стараются найти предложения квартир на любом этаже за исключением первого и последнего, за частую это высотные здания и новостройки. В центре же города ситуция обратная, застройки практически никакой не ведётся, этажность домов не большая и продажа квартир ведётся из старого фонда, что и отражается на снижающемся спросе начиная с 2016 года.

Резюмируя итог исследовательской работы мы имеем, что в большинстве из случаев продаются 1-2 комнатные квартиры старого фонда хрущевской застройки за полугодичный срок, со средней стоимостью в 115 т.р. за кв.метр. и высотой потолков 2,65м.. Меньше всего предложений по продаже недвижимости это в высотных новостроек по периметру Санкт-Петребурга. Таким образом можно сделать вывод о том, что количество таких домов и соответственно предложений будет расти в след за ценой в отличии от цен на жильё расположенного по центру исторического района города в невысоких домах из старого фонда.

Чек-лист готовности проекта

Поставьте 'x' в выполненных пунктах. Далее нажмите Shift+Enter.

  • открыт файл
  • файлы изучены (выведены первые строки, метод info(), гистограммы и т.д.)
  • определены пропущенные значения
  • заполнены пропущенные значения там, где это возможно
  • есть пояснение, какие пропущенные значения обнаружены
  • изменены типы данных
  • есть пояснение, в каких столбцах изменены типы и почему
  • устранены неявные дубликаты в названиях населённых пунктов
  • устранены редкие и выбивающиеся значения (аномалии) во всех столбцах
  • посчитано и добавлено в таблицу: цена одного квадратного метра
  • посчитано и добавлено в таблицу: день публикации объявления (0 - понедельник, 1 - вторник и т.д.)
  • посчитано и добавлено в таблицу: месяц публикации объявления
  • посчитано и добавлено в таблицу: год публикации объявления
  • посчитано и добавлено в таблицу: тип этажа квартиры (значения — «первый», «последний», «другой»)
  • посчитано и добавлено в таблицу: расстояние в км до центра города
  • изучены и описаны следующие параметры: - общая площадь; - жилая площадь; - площадь кухни; - цена объекта; - количество комнат; - высота потолков; - этаж квартиры; - тип этажа квартиры («первый», «последний», «другой»); - общее количество этажей в доме; - расстояние до центра города в метрах; - расстояние до ближайшего аэропорта; - расстояние до ближайшего парка; - день и месяц публикации объявления
  • построены гистограммы для каждого параметра
  • выполнено задание: "Изучите, как быстро продавались квартиры (столбец days_exposition). Этот параметр показывает, сколько дней «висело» каждое объявление.
    • Постройте гистограмму.
    • Посчитайте среднее и медиану.
    • В ячейке типа markdown опишите, сколько обычно занимает продажа. Какие продажи можно считать быстрыми, а какие — необычно долгими?"
  • выполнено задание: "Какие факторы больше всего влияют на общую (полную) стоимость объекта? Постройте графики, которые покажут зависимость цены от указанных ниже параметров. Для подготовки данных перед визуализацией вы можете использовать сводные таблицы." - общей площади; - жилой площади; - площади кухни; - количество комнат; - типа этажа, на котором расположена квартира (первый, последний, другой); - даты размещения (день недели, месяц, год);
  • выполнено задание: "Посчитайте среднюю цену одного квадратного метра в 10 населённых пунктах с наибольшим числом объявлений. Выделите населённые пункты с самой высокой и низкой стоимостью квадратного метра. Эти данные можно найти по имени в столбце locality_name."
  • выполнено задание: "Ранее вы посчитали расстояние до центра в километрах. Теперь выделите квартиры в Санкт-Петербурге с помощью столбца locality_name и вычислите среднюю цену каждого километра. Опишите, как стоимость объектов зависит от расстояния до центра города."
  • в каждом этапе есть промежуточные выводы
  • есть общий вывод
Дополнительные ссылки ⚠️:

Могу посоветовать для начинающих аналитиков посмотреть данные материалы, чтобы научиться оформлять графики еще лучше:

Здесь

И здесь

И вот тут